import { h, Schema } from "koishi"
import crypto from "crypto"

export const name = "cychat-bridge"

export const inject = {
  required: ["database"], // user name
  optional: ["assets", "puppeteer"],
}

export const Config = Schema.object({
  endpoint: Schema.string().default("https://vudrux.site/c/api.php").role("url"),
  self: String,
  password: String,
  relay: Schema.object({
    platform: String,
    selfId: String,
    channelId: String,
    gulidId: Schema.string(),
  }),
  assetsBaseUrl: Schema.string(),
})

class CychatError extends Error {
  name = "CychatError"
  constructor(action, code, message) {
    super(`Error ${code} with request ${action}: ${message}`)
  }
}

export function apply(ctx, config) {
  const { endpoint, self, password, relay } = config
  ctx = ctx.intercept("http", { proxyAgent: null })

  async function request(action, data, key) {
    const time = Math.floor(Date.now() / 1000)
    const meta = [self, time, action, key].join(",")
    ctx.logger.debug("%o", meta)
    const signature = crypto.createHmac("md5", password).update(meta).digest("hex")
    data = {
      action,
      self,
      time,
      signature,
      ...data,
    }
    ctx.logger.debug("request", { ...data, meta })
    const response = await ctx.http.post(endpoint, data)
    ctx.logger.debug("response", response)
    if (response.error) throw new CychatError(action, response.error, response.message)
    return response.result
  }

  ctx.on("ready", async () => {
    //ctx.logger.debug(await request("list_messages", {}, []))

    let lastId = null
    let errCount = 0
    let r = async () => {
      const init = lastId === null
      let messages
      try {
        messages = await request(
          "list_messages",
          init ? {} : { after: lastId },
          init ? [] : ["after", lastId],
        )
      } catch (e) {
        errCount++
        if (++errCount < 3) ctx.logger.error(e)
        else ctx.logger.debug(e)
      }
      if (messages?.length) {
        if (!init) {
          for (const message of messages)
            await relayMessage(message).catch(e => ctx.logger.error(e))
        }
        lastId = messages.at(-1).id
      }
    }
    r = (r_ => () => r_().finally(() => { ctx.setTimeout(r, 4000) }))(r)
    ctx.setTimeout(r, 0)

    async function relayMessage(message) {
      let { from_user, to_user, content, ctime } = message
      if (to_user === self) {
        const content = "LNNBot 在线，当前服务器时间为 " + new Date().toLocaleString("zh-CN")
        request(
          "create_message",
          { target: from_user, content },
          [from_user, content],
        ).catch(e => ctx.logger.warn(e))
        return
      }
      if (from_user === self || to_user) return
      if (from_user.match(/bot$/i)) return
      if (content.includes("<") && ctx.puppeteer)
        content = await ctx.puppeteer.render(`<style>:root{font-family: -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", "Source Han Sans SC", "Source Han Sans CN", "Noto Sans SC", "Noto Sans CJK SC", "PingFang SC", system-ui, "Microsoft YaHei UI", "Microsoft YaHei", "微软雅黑", sans-serif}img,video{max-width:20rem;max-height:30rem}</style><div style="display:inline-block;white-space:pre-wrap">${content}</div>`, async (p, n) => n(await p.$("div")))
      return ctx.bots[`${relay.platform}:${relay.selfId}`].sendMessage(
        relay.channelId,
        `<b>\u2068${from_user}\u2069:</b> \u2068${content}`,
        relay.guildId,
      )
    }

    ctx
      .platform(relay.platform)
      .channel(relay.channelId)
      .middleware(async (session, next) => {
        if (!(session.content[0] === ":" || session.content[0] === "：")) return next()
        await session.observeUser(["name"])
        let content = session.content.slice(1)
        const author =
          session.user?.name ||
          session.author?.nick ||
          session.event?.user?.nick ||
          session.author?.name ||
          session.userId
        const m = {
          text: true,
          b: true,
          i: true,
          br: true,
          p: true,
          at: a => h("i", `@${a.name || a.role || a.type || a.id}`),
          sharp: a => h("i", `#${a.name || a.role || a.type || a.id}`),
          img: a => (a.src || a.url) ? h.image(a.src || a.url) : null,
          image: a => (a.src || a.url) ? h.image(a.src || a.url) : null,
          default: (a, c) => h.transform(c, m),
        }
        content = h.transform(content, m)
        if (!content.trim() && !session.event?.message?.quote) return next()
        content = `<strong><bdi>${h.text(author)}</bdi>: </strong>${content}`
        if (session.event?.message?.quote) {
          const qAuthor =
            session.event.message.quote.user && (await ctx.database.getUser(session.platform, session.event.message.quote.user.id, ["name"]).catch(() => null))?.name ||
            session.event.message.quote.member?.nick ||
            session.event.message.quote.user?.nick ||
            session.event.message.quote.member?.name ||
            session.event.message.quote.user?.name ||
            session.event.message.quote.user?.id ||
            "?"
          let qContent = h.transform(session.event.message.quote.content.replace(/^[:：]/, ""), m)
          if (!qContent.trim()) qContent = "<i>未知消息</i>"
          content = `<blockquote style="color:#aaa;font-size:.8em;border-left:3px solid #aaa;margin:0.25em 0;padding-left:0.25em"><strong><bdi>${h.text(qAuthor)}</bdi>: </strong>${qContent}</blockquote>` + content
        }
        if (ctx.assets) {
          content = await ctx.assets.transform(h.normalize(content))
          if (config.assetsBaseUrl)
            content = h.transform(content, {
              img: a => (
                a.src.startsWith(ctx.assets.baseUrl)
                ? h.image(a.src.replace(ctx.assets.baseUrl, config.assetsBaseUrl))
                : true
              ),
            })
        }
        content = content.join("")
        request(
          "create_message",
          { target: null, content },
          ["", content],
        ).catch(e => ctx.logger.error(e))
        return next()
      })
  })
}
